{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "# Multinetwork storage optimization with PandaModels.jl\n",
    "This tutorial describes how to run a storage optimization over multiple timesteps with a PandaModels.jl multinetwork\n",
    "together with pandapower.\n",
    "\n",
    "To run a storage optimization over multiple time steps, the power system data is copied n_timestep times internally.\n",
    "This is done efficiently in a julia script. Each network in the multinetwork dict represents a single time step. \n",
    "The input time series must be written to the loads and generators accordingly to each network. \n",
    "This is currently done by converting input time series to pandapwower controllers, saving it together with the grid data as a json file and loading the data back in julia. This \"hack\" is probably just a temporary solution. \n",
    "\n",
    "Some notes:\n",
    "* only storages which are set as \"controllable\" are optimized\n",
    "* time series can be written to load / sgen elements\n",
    "* output of the optimization is a dict containing pandas DataFrames for every optimized storage and time step   \n",
    "\n",
    "For more details on PowerModels (PandaModels) storage model see:\n",
    "\n",
    "https://lanl-ansi.github.io/PowerModels.jl/stable/storage/ and \n",
    "https://github.com/e2nIEE/PandaModels.jl/blob/develop/src/models/call_powermodels.jl\n",
    "\n",
    "For more details on PowerModels multinetworks see:\n",
    "\n",
    "https://lanl-ansi.github.io/PowerModels.jl/stable/multi-networks/\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Installation\n",
    "You need the standard Julia, PowerModels, Ipopt and JuMP Installation (see the opf_powermodels.ipynb).\n",
    " "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Run the storage optimization\n",
    "In order to start the optimization and visualize results, we follow four steps:\n",
    "1) Load the pandapower grid data (here the cigre MV grid)\n",
    "2) Convert the time series to pandapwoer-controllers\n",
    "3) Start the optimization\n",
    "4) Get and plot the results\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## 1) Get the grid data\n",
    "We load the cigre medium voltage grid with \"pv\" and \"wind\" generators. Also we set some limits and add a storage with\n",
    "**controllable** == True\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "from pandapower.networks.cigre_networks import create_cigre_network_mv\n",
    "from pandapower.create import create_storage\n",
    "from pandapower.runpm import runpm_storage_opf, read_pm_storage_results\n",
    "from pandapower.control import ConstControl\n",
    "from pandapower.timeseries import DFData"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def cigre_grid():\n",
    "    net = create_cigre_network_mv(\"pv_wind\")\n",
    "    # set some limits\n",
    "    min_vm_pu = 0.95\n",
    "    max_vm_pu = 1.05\n",
    "\n",
    "    net[\"bus\"].loc[:, \"min_vm_pu\"] = min_vm_pu\n",
    "    net[\"bus\"].loc[:, \"max_vm_pu\"] = max_vm_pu\n",
    "\n",
    "    net[\"line\"].loc[:, \"max_loading_percent\"] = 100.\n",
    "\n",
    "    # close all switches\n",
    "    net.switch.loc[:, \"closed\"] = True\n",
    "    # add storage to bus 10\n",
    "    create_storage(net, 10, p_mw=0.5, max_e_mwh=.2, soc_percent=0., q_mvar=0., controllable=True)\n",
    "\n",
    "    return net"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2) Convert the time series to pandapower-controllers\n",
    "The following functions loads the example time series from the input_file and scales the power accordingly.\n",
    "It then adds the time series data to the grid model by creating controllers.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "def convert_timeseries_to_controller(net, input_file):\n",
    "    \n",
    "    # set the load type in the cigre grid, since it is not specified\n",
    "    net[\"load\"].loc[:, \"type\"] = \"residential\"\n",
    "    \n",
    "    # set the sgen type in the cigre grid\n",
    "    net.sgen.loc[:, \"type\"] = \"pv\"\n",
    "    net.sgen.loc[8, \"type\"] = \"wind\"\n",
    "\n",
    "    # read the example time series\n",
    "    time_series = pd.read_json(input_file)\n",
    "    time_series.sort_index(inplace=True)\n",
    "\n",
    "    # this example time series has a 15min resolution with 96 time steps for one day\n",
    "    n_timesteps = time_series.shape[0]\n",
    "    \n",
    "    # get rated power\n",
    "    load_p = net[\"load\"].loc[:, \"p_mw\"].values\n",
    "    sgen_p = net[\"sgen\"].loc[:7, \"p_mw\"].values\n",
    "    wind_p = net[\"sgen\"].loc[8, \"p_mw\"]\n",
    "\n",
    "    load_ts = pd.DataFrame(index=time_series.index.tolist(), columns=net.load.index.tolist())\n",
    "    sgen_ts = pd.DataFrame(index=time_series.index.tolist(), columns=net.sgen.index.tolist())\n",
    "    sgen_cols = sgen_ts.columns\n",
    "    for t in range(n_timesteps):\n",
    "        load_ts.loc[t] = load_p * time_series.at[t, \"residential\"]\n",
    "        sgen_ts.loc[t, sgen_cols[:8]] = sgen_p * time_series.at[t, \"pv\"]\n",
    "        sgen_ts.loc[t, sgen_cols[8]] = wind_p * time_series.at[t, \"wind\"]\n",
    "\n",
    "    # create time series controller for load and sgen \n",
    "    ConstControl(net, element=\"load\", variable=\"p_mw\",\n",
    "                 element_index=net.load.index.tolist(), profile_name=net.load.index.tolist(),\n",
    "                 data_source=DFData(load_ts))\n",
    "    ConstControl(net, element=\"sgen\", variable=\"p_mw\",\n",
    "                 element_index=net.sgen.index.tolist(), profile_name=net.sgen.index.tolist(),\n",
    "                 data_source=DFData(sgen_ts))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3) Start the optimization \n",
    "Before we start the optimization, we create the grid and controller, adding the time series in 15min resolution. \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# open the cigre mv grid\n",
    "net = cigre_grid()\n",
    "\n",
    "# convert the time series to pandapower controller\n",
    "input_file = \"cigre_timeseries_15min.json\"\n",
    "convert_timeseries_to_controller(net, input_file)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, the time series is added through (const) controllers, and you can check the created controllers\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- time series controller:\", net.controller)\n",
    "\n",
    "print(\"--- considered element of controller 0:\", net.controller.object[0].__dict__[\"matching_params\"][\"element\"])\n",
    "print(\"--- considered element index of controller 0:\",net.controller.object[0].__dict__[\"matching_params\"][\"element_index\"])\n",
    "print(\"--- time series data:\",net.controller.object[0].data_source.df)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We start the optimization for timesteps from 0 to 10.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# run the optimization for the first ten timesteps (the first run can be slow).\n",
    "try:\n",
    "    runpm_storage_opf(net, from_time_step=0, to_time_step=10, pm_mip_solver='cbc')\n",
    "except Exception as err:\n",
    "    print(err)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4) Get and plot the results \n",
    "Get and plot the optimization results for the storage.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "def plot_storage_results(storage_results):\n",
    "    n_res = len(storage_results.keys())\n",
    "    fig, axes = plt.subplots(n_res, 2)\n",
    "    if n_res == 1:\n",
    "        axes = [axes]\n",
    "    for i, (key, val) in enumerate(storage_results.items()):\n",
    "        res = val\n",
    "        axes[i][0].set_title(\"Storage {}\".format(key))\n",
    "        el = res.loc[:, [\"p_mw\", \"q_mvar\", \"soc_mwh\"]]\n",
    "        el.plot(ax=axes[i][0])\n",
    "        axes[i][0].set_xlabel(\"time step\")\n",
    "        axes[i][0].legend(loc=4)\n",
    "        axes[i][0].grid()\n",
    "        ax2 = axes[i][1]\n",
    "        patch = plt.plot([], [], ms=8, ls=\"--\", mec=None, color=\"grey\", label=\"{:s}\".format(\"soc_percent\"))\n",
    "        ax2.legend(handles=patch)\n",
    "        ax2.set_label(\"SOC percent\")\n",
    "        res.loc[:, \"soc_percent\"].plot(ax=ax2, linestyle=\"--\", color=\"grey\")\n",
    "        ax2.grid()\n",
    "    plt.show()\n",
    "\n",
    "# get the results\n",
    "storage_results = read_pm_storage_results(net)\n",
    "    \n",
    "# plot the results\n",
    "plot_storage_results(storage_results)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3"
  },
  "pycharm": {
   "stem_cell": {
    "cell_type": "raw",
    "metadata": {
     "collapsed": false
    },
    "source": []
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
